<!doctype html> 
<html> 
	<head> 
		<title>HashNet Peer Visulization</title> 
		<meta http-equiv="content-type" content="text/html;charset=utf-8" />
		
		
		<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/smoothness/jquery-ui.css" />
		<link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.1/jquery.mobile-1.1.1.min.css"/>
		
		<link rel="stylesheet" type="text/css" href="http://dev.sencha.com/deploy/ext-4.1.0-gpl/resources/css/ext-all.css" /> 
		<link rel="stylesheet" type="text/css" href="http://dev.sencha.com/deploy/ext-4.1.0-gpl/examples/shared/example.css" /> 
 
		
		
		<style>
			body { 
				background-color:white; font-size:90%;
				/*background-image:url(../img/dynamicAnchorBg.jpg); */
				font-family:Helvetica;
				padding: 1em;
			}
			
			#header {
				color:rgb(220,220,220);
				font-size:4em;
				margin:0;
				text-shadow: 0.0em 0.0em .1em rgb(220,220,220);
			}
			
			#header img
			{
				opacity:0.20;
				filter:alpha(opacity=20);
			}
			
			
			.shape {
				z-index:2;
				opacity:0.7;
				filter:alpha(opacity=70);
				position:absolute;
				cursor:pointer;
			}
			.shape .name {
				text-align:center;width:100%;
			}
			[data-shape=circle] {
				width:150px;
				height:150px;
				background-image:url(/images/circle.png);
			}
			
			[data-shape=rectangle] {
				width:150px;
				height:50px;
				background: rgb(220,220,220);
				border: 2px solid rgb(50,50,50);
			}
			
			path, ._jsPlumb_endpoint { cursor:default; z-index:2 }
			
			._jsPlumb_connector {
				z-index:1;
			}
			
			._jsPlumb_overlay {
				z-index:2;
			}
			
			.peer_info {
				text-shadow: 0.0em 0.0em 0.5em #333;
				border: 1px solid black;
				background: white;
				padding: 3px;
				opacity:0.7;
				filter:alpha(opacity=70);
				font-size: 10px;
				font-family: Monospace;
				white-space:nowrap;
			}
			
			#loading_msg {
				z-index: 999;
				width:20em;
				padding: .5em 2em 2em 2em;
			}
			
	
			


		</style>
	</head> 
	<body onunload="jsPlumb.unload();">
		
		<div style='float:right;font-size:.85em' id='last-updated'>
			<span class=date></span><br>
			<a href='#' onclick='showChart();$(this).remove();return false;'>Show Chart</a>
			
		</div>
		<h1 id=header><a href='/'><img src='/hashnet-logo.png' border=0 align='absmiddle' style='margin-top:-15px'></a> HashNet StorageEngine Server</h1>

		<div style="position:absolute;top:0;left:0">
			<div id="viz"></div> 
		</div> 

		<!-- http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js fails to load properly -->
		<!-- Update 9/13/12: Loads now...-->
		<!--<script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js'></script>-->
		<script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js'></script>
		
		<!-- jsPlumb fails with 1.8.1, so switching to code.jquery.com to get the old 1.7.1 -->
		<!--<script type='text/javascript' src='http://code.jquery.com/jquery-1.7.1.min.js'></script>-->
		
		<script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.13/jquery-ui.min.js'></script>
		 
		<script type="text/javascript" charset="utf-8" src="http://cdn.sencha.io/ext-4.1.0-gpl/ext-all.js"></script>
				
		<script type='text/javascript' src='/js/jquery.jsPlumb-1.3.13-all-min.js'></script>
		<!--<script type='text/javascript' src='/js/jquery.jsPlumb-1.3.13-all.js'></script>--> 
		
		<script>
		Ext.require('Ext.chart.*');

		window.chartData = { init: false }; // node_uuid_map: node_uuid_map };
		window.showChart = function()
		{
			//window.chartData = { init: true, node_uuid_map: node_uuid_map };
			window.chartData.init = true;
			var map_list = chartData.map_list;
			var node_uuid_map = chartData.node_uuid_map;
			
			var chart;
			var generateData = (function() 
			{
				var data = [], i = 0,
					last = false,
					date = new Date(), //2011, 1, 1, 12, 00, 00),
					seconds = +date,
					min = Math.min,
					max = Math.max,
					random = Math.random,
					lastData = false;
				
					
				return function() 
				{
					data = data.slice();
					var ref = {
						date:	new Date(), //Ext.Date.add(date, Ext.Date.MINUTE, i++),
						//date:	Ext.Date.add(date, Ext.Date.MINUTE, i++),
						//visits:	min(100, max(last? last.visits + (random() - 0.5) * 20 : random() * 100, 0)),
						//views:	min(100, max(last? last.views + (random() - 0.5) * 10 : random() * 100, 0)),
						//users:	min(100, max(last? last.users + (random() - 0.5) * 20 : random() * 100, 0))
					};
					//console.debug(ref.date);
					date = ref.date;
					
					for(var uuid in node_uuid_map)
					{
						//ref[uuid] = min(100, max(last? last[uuid] + (random() - 0.5) * 20 : random() * 100, 0))
						var delta = chartData.node_uuid_map[uuid].cur_tx_id - lastData[uuid];
						if(delta == 0 && last && last[uuid] > 0)
							delta = last[uuid];
						ref[uuid] = lastData ? delta : 0;
						//console.debug(uuid, "delta: ", delta, ", last: ", lastData[uuid]);
					}
					
					lastData = {};
					for(var uuid in node_uuid_map)
					{
						lastData[uuid] = chartData.node_uuid_map[uuid].cur_tx_id;
					}
					
					data.push(ref);
					
					last = data[data.length -1];
					return data;
				};
			})();
			
			
// 			var group = false,
// 				groupOp =
// 				[
// 					{
// 						dateFormat: 'M d h m s',
// 						groupBy: 'year,month,day,hour,minute second'
// 					},
// 					{
// 						dateFormat: 'M d h m',
// 						groupBy: 'year,month,day,hour'
// 					}
// 				];
// 		
// 			function regroup()
// 			{
// 				group = !group;
// 				var axis = chart.axes.get(1),
// 					selectedGroup	= groupOp[+group];
// 					axis.dateFormat	= selectedGroup.dateFormat;
// 					axis.groupBy	= selectedGroup.groupBy;
// 					
// 				chart.redraw();
// 			}
		
			var fieldList = ['date'];
			var uuidList = [];
			var seriesList = [];
			for(var uuid in node_uuid_map)
			{
				fieldList.push(uuid);
				uuidList.push(uuid);
				
				seriesList.push(
				{
					type: 'line',
					smooth: true,
					axis: ['left', 'bottom'],
					xField: 'date',
					yField: uuid,
					title: node_uuid_map[uuid].name,
					//displayName: node_uuid_map[uuid].name,
					label: {
						display: 'none',
						field: uuid,
						renderer: function(v) { return v >> 0; },
						'text-anchor': 'middle'
					},
					markerConfig:
					{
						radius: 5,
						size: 5
					},
					
				});
			}
			
			
			var store = Ext.create('Ext.data.JsonStore',
			{
				//fields: ['date', 'visits', 'views', 'users'],
				fields: fieldList,
				data: generateData()
			});
		
			var getData = function() 
			{
				var gs = generateData();
				var toDate = timeAxis.toDate,
				    lastDate = gs[gs.length - 1].date,
				    markerIndex = chart.markerIndex || 0;
				
				if (+toDate < +lastDate)
				{
					markerIndex = 1;
					timeAxis.toDate = lastDate;
					//timeAxis.fromDate = Ext.Date.add(Ext.Date.clone(timeAxis.fromDate), Ext.Date.MINUTE, 1);
					timeAxis.fromDate = Ext.Date.add(Ext.Date.clone(timeAxis.toDate), Ext.Date.MINUTE, -5);
					//toDate: Ext.Date.add(new Date(), Ext.Date.MINUTE, 1),
					//console.debug(timeAxis.fromDate, timeAxis.toDate);
					chart.markerIndex = markerIndex;
				}
				store.loadData(gs);
				
			};
			var intr = setInterval(getData, 15 * 1000); // * 1000);
			setTimeout(getData, 1000);
		
			Ext.create('Ext.Window',
			{
				width: 500,
				height: 300,
				minHeight: 200,
				minWidth: 250,
				hidden: false,
				maximizable: true,
				title: 'Transaction Activity',
				renderTo: Ext.getBody(),
				layout: 'fit',
				opacity: 0.7,
				
				items: [
				{
					xtype: 'chart',
					style: 'background:#fff',
					id: 'chartCmp',
					store: store,
					shadow: false,
					animate: true,
					
					legend: {
						position: 'right'
					},
				
					
					axes:
					[
						{
							type: 'Numeric',
							grid: true,
							minimum: 0,
							//maximum: 50,
							position: 'left',
							fields: uuidList, //['views', 'visits', 'users'],
							title: 'Number of Transactions',
							grid: {
								odd: {
									fill: '#dedede',
									stroke: '#ddd',
									'stroke-width': 0.5
								}
							}
						},
						{
							type: 'Time',
							position: 'bottom',
							fields: 'date',
							title: 'Time',
							dateFormat: 'M d h m s',
							groupBy: 'year,month,day,hour,minute',
							aggregateOp: 'sum',
							constrain: true,
							fromDate: new Date(), //new Date(2011, 1, 1),
							toDate: Ext.Date.add(new Date(), Ext.Date.MINUTE, 5), //new Date(2011, 1, 7),
							//toDate: Ext.Date.add(new Date(), Ext.Date.SECOND, 3), //new Date(2011, 1, 7),
							grid: true
						}
					],
					
					series: seriesList,
// 					[
// 						{
// 							type: 'line',
// 							smooth: false,
// 							axis: ['left', 'bottom'],
// 							xField: 'date',
// 							yField: 'visits',
// 							label: {
// 								display: 'none',
// 								field: 'visits',
// 								renderer: function(v) { return v >> 0; },
// 								'text-anchor': 'middle'
// 							},
// 							markerConfig:
// 							{
// 								radius: 5,
// 								size: 5
// 							}
// 						},
// 						{
// 							type: 'line',
// 							axis: ['left', 'bottom'],
// 							smooth: false,
// 							xField: 'date',
// 							yField: 'views',
// 							label: {
// 								display: 'none',
// 								field: 'visits',
// 								renderer: function(v) { return v >> 0; },
// 								'text-anchor': 'middle'
// 							},
// 							markerConfig: {
// 								radius: 5,
// 								size: 5
// 							}
// 						},
// 						{
// 							type: 'line',
// 							axis: ['left', 'bottom'],
// 							smooth: false,
// 							xField: 'date',
// 							yField: 'users',
// 							label: {
// 								display: 'none',
// 								field: 'visits',
// 								renderer: function(v) { return v >> 0; },
// 								'text-anchor': 'middle'
// 							},
// 							markerConfig:
// 							{
// 								radius: 5,
// 								size: 5
// 							}
// 						}
// 					]
					
					_foobar: {}
				}]
			});
			
			chart = Ext.getCmp('chartCmp');
			var timeAxis = chart.axes.get(1);
			
		};
		
		//Ext.onReady(showChart);
		
		;(function() {

			function verticalCenter(str)
			{
				// Vertical centering solution based on http://www.jakpsatweb.cz/css/css-vertical-center-solution.html
				return '<div class="name" style="display: table; height: 100%; #position: relative; overflow: hidden">'
					+ '<div style=" #position: absolute; #top: 50%;display: table-cell; vertical-align: middle;">'
						+ '<div style=" #position: relative; #top: -50%">'+str+'</div></div></div>';
			}
			
			// Cookies
			function createCookie(name, value, days) {
				if (days == undefined)
					days = 365 * 99;
				if (days) {
					var date = new Date();
					date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
					var expires = "; expires=" + date.toGMTString();
				}
				else var expires = "";
			
				var newCookie = name + "=" + value + expires + "; path=/";
				//console.debug(newCookie);
				document.cookie = newCookie;
			}
		
			function readCookie(name) {
				var nameEQ = name + "=";
				var ca = document.cookie.split(';');
				//console.debug("readCookie: ", name,": ", ca);
				for (var i = 0; i < ca.length; i++) {
					var c = ca[i];
					while (c.charAt(0) == ' ') c = c.substring(1, c.length);
					if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
				}
				return null;
			}
		
			function eraseCookie(name) {
				createCookie(name, "", -1);
			}
			
			window.showLoading = function()
			{
				// Disable for now
				return;
				
				//console.debug("Connection Lost...");
				if(window.LoadingVisible)
					return;
					
				window.LoadingVisible = true;
		
				//$.mobile.showPageLoadingMsg("b","Connection Lost, Reconnecting...");
				var elm = $("<div class='ui-body ui-body-a' id='loading_msg'><center><h3>Loading</h3>Loading data from server...</center></div>");
				$(document.body).append(elm);
				window.showLoading.elm = elm;
				
				var win = $(window);
				elm.css('top', win.height() /2 - elm.height()/2 + $(window).scrollTop() );
				elm.css('left', win.width() /2 - elm.width() /2 );
			}
			
			window.hideLoading = function()
			{
				if(window.LoadingVisible)
				{
					window.LoadingVisible = false;
					//$.mobile.hidePageLoadingMsg();
					window.showLoading.elm.remove();
				}
			}

			window.pollServer = function()
			{
			
				showLoading();
				
				$.ajax({
					type: "GET",
					url: '/db/viz/nodeinfo.js',
					error: function(data)
					{
						//console.debug(url + ":error:",data);
						//showConLost();
						hideLoading();
					},
					success: function(data)
					{
						$("#last-updated .date").html("<i>Last updated "+(new Date())+"</i>");
						
						var map_list = data;
						if(typeof(map_list) != 'object')
						{
							eval("map_list="+map_list);
						}
						
						var $container = $("#viz");
						
						// Remove all inner blocks
						$container.children().each(function() {
							$(this).remove();
						});
						
						
						var node_uuid_map = {};
						for(var idx in map_list)
						{
							var node_info = map_list[idx];
							
							var nx = node_info.x, ny = node_info.y;
							
							/*
							var userSetPosition = readCookie(node_info.uuid+".position");
							if(userSetPosition)
							{
								var list = userSetPosition.split(",");
								nx = parseFloat(list[0]);
								ny = parseFloat(list[1]);
							}
							*/

							var geoList = node_info.geo_info.split(", ");
							// Lat         Lng
							geoList.pop(); geoList.pop();
							// Country
							geoList.pop();
							
							var geoString = geoList.join(", ");
							
							var nodeInfoHtml = ""
								+ "<b>" + node_info.name + "</b><br>"
								+ geoString + "<br>"
								+ "<i>" + node_info.hashnet_ver
									+ (node_info.port != 8031 ? ":" + node_info.port : "") // only show port if not default hashnet port
									+ " ("+node_info.cur_tx_id+")"
									+ "</i>";
							
							//var $shape = $('<div class="shape" data-shape="rectangle" style="left:'+nx+'px;top:'+ny+'px;">'+genNameHtml(node_info.name)+'</div>');
							var $shape = $('<div class="shape" data-shape="circle" style="left:'+nx+'px;top:'+ny+'px;">'+verticalCenter(nodeInfoHtml)+'</div>');
							$container.append($shape);
							
							$shape.attr('uuid', node_info.uuid);
							
							$shape.bind( "dragstop", function(event, ui) {
								var pos  = $(this).offset();
								var uuid = $(this).attr('uuid');
								//console.debug(uuid, pos.left,pos.top);
								createCookie(uuid+".position", pos.left+','+pos.top);
								$.ajax({
									type: "POST",
									url: '/db/viz/store_pos',
									data:
									{
										uuid: uuid,
										x:    pos.left,
										y:    pos.top,
									},
									success: function(data)
									{
									}
								});
							});
							
							node_info.shapeRef = $shape;
							node_uuid_map[node_info.uuid] = node_info;
						}
						
						jsPlumb.importDefaults({
							Connector:"StateMachine",
							//Connector:"Flowchart",
							//Connector: [ "Bezier", { curviness:500 } ],
							PaintStyle:{ lineWidth:3, strokeStyle:"#ffa500", "dashstyle":"2 4" },
							Endpoint:[ "Dot", { radius:5 } ],
							EndpointStyle:{ fillStyle:"#ffa500" }
						});
						
						var con_created = {};
						
						var cd = new Date();//"Thu Sep 06 2012 14:12:06 GMT-0400 (EDT)");
						//console.debug(cd);
						//console.debug(map_list);
						var numHosts = map_list.length;
						
						window.chartData.node_uuid_map = node_uuid_map;
						window.chartData.map_list = map_list;
						
						//if(!window.chartData.init)
						//	showChart();
						
						for(var idx in map_list)
						{
							var node_info = map_list[idx];
							var node_uuid = node_info.uuid;
						
							//if(idx == 0)
							// console.debug(node_info.uuid, node_info.cur_tx_id);
							
							for(var link_idx in node_info.links)
							{
								var link_info = node_info.links[link_idx];
								var link_uuid = link_info.uuid;
								var peer_info = node_uuid_map[link_info.uuid];

								if(!peer_info)
								{
									if(window.console && window.console.debug)
									{
										console.debug("link ", link_info, " does not exist in node_uuid_map: ", node_uuid_map);
										continue;
									}
								}
								
								var con_key = link_uuid>node_uuid ? link_uuid+'|'+node_uuid : node_uuid+'|'+link_uuid;
								//console.debug(con_key);
								
								if(!con_created[con_key])
								{
									var $src  = node_info.shapeRef;
									var $dest = peer_info.shapeRef;
									
									//con_created[con_key] = true;
									
									var pd = new Date(link_info.last_seen);
									
									//var diffMs = Math.abs(cd - pd);
									//var diffMin = diffMs / 1000 / 60;
									var diffMs = Math.abs(pd - cd); // milliseconds between now & Christmas
									var diffDays = parseInt(diffMs / 86400000); // days
									var diffHrs = parseInt((diffMs % 86400000) / 3600000); // hours
									var diffMins = parseInt(((diffMs % 86400000) % 3600000) / 60000); // minutes
									var diffSecs = parseInt((((diffMs % 86400000) % 3600000) % 60000) / 1000); // seconds
									
									var agoList = [];
									if(diffDays > 0)
										agoList.push(diffDays+" day"+(diffDays>1?"s":""));
									else
									{
										if(diffHrs > 0)
											agoList.push(diffHrs+" hr"+(diffHrs>1?"s":""));
										if(diffMins > 0)
											agoList.push(diffMins+" min");
										if(diffSecs > 0 && diffMins <= 0)
											agoList.push(diffSecs+" sec");
										if(agoList.length <= 0)
											agoList.push("Less than 1 sec");
									}
									
									var timeAgo = agoList.length <= 0 ? "" : (agoList.join(", ") + " ago");
									var maxMinutes = 1000 * 60 * 10;
									
									// This will prevent the 'yellow' style from showing
									//if(diffMs > maxMinutes)
									//	link_info.host_down = 1;
										
									var latencyString = 
										link_info.host_down > 0 ? "<b style='color:red'>Host Down</b>" :
										link_info.latency   > 0 ? sprintf("%d ms latency",Math.round(link_info.latency * 1000))
										: "Unknown latency";
									
									
									var txStatus = '';
									if(link_info.last_tx_sent)
									{
										var diff = node_info.cur_tx_id - link_info.last_tx_sent;
										// We define maxLateTx as the numHosts*numHosts because the number of tx queued during a normal cycle could be *at least* as many as their are hosts,
										// *2 update periods, *2 more for buffer
										var maxLateTx = Math.pow(numHosts, 4) * 4;
										//alert(diff);
										txStatus = diff == 0 ? "Data current " : "<span style='color:" + (Math.abs(diff) > maxLateTx ? 'red' : 'green')+"'>"
											+ Math.abs(diff)
											//+ '/' + maxLateTx
											+ (diff > 0 ? ' tx behind' : ' tx ahead') + '</span> ';
									}
										
									//console.debug(diffMs, diffDays,diffHrs,diffMins);
									
									jsPlumb.connect({
										source: $src.get(0),  // just pass in the current node in the selector for source 
										target: $dest.get(0),
										//here we supply a different anchor for source and for target, and we get the element's "data-shape"
										//attribute to tell us what shape we should use.
										anchors:
										[
											[ "Perimeter", { shape: $src.attr("data-shape")  }],
											[ "Perimeter", { shape: $dest.attr("data-shape") }]
										],
										
										paintStyle:
											link_info.host_down>0 ? { lineWidth:1, strokeStyle:"red",    "dashstyle":"1 10" } :
											diffMs > maxMinutes ?   { lineWidth:3, strokeStyle:"yellow", "dashstyle":"4 4" } :
														{ lineWidth:3, strokeStyle:"green", /* "dashstyle":"2 4" */},
										
										endpointStyle:{ fillStyle: link_info.host_down>0 ? "red" : diffMs > maxMinutes ? "yellow": "green" },
										
										overlays:[
											[ "Label", { label: txStatus + timeAgo+"<br>"+latencyString, location: 0.1, cssClass: "peer_info" } ],
										]
									});
									
									
								}
							}
						}
						
						// NOTE here we are just using getSelector so we don't have to rewrite the code for each of the supported libraries.
						// you can just use the approriate selector from the library you're using, if you want to. like $(".shape) on jquery, for example.
						var shapes = jsPlumb.getSelector(".shape");
							
						// make everything draggable
						jsPlumb.draggable(shapes);
						
						hideLoading();
						
	// 						
	// 					// loop through them and connect each one to each other one.
	// 					for (var i = 0; i < shapes.length; i++)
	// 					{
	// 						for (var j = i + 1; j < shapes.length; j++)
	// 						{
	// 							jsPlumb.connect({
	// 								source: shapes[i],  // just pass in the current node in the selector for source 
	// 								target: shapes[j],
	// 								// here we supply a different anchor for source and for target, and we get the element's "data-shape"
	// 								// attribute to tell us what shape we should use.
	// 								anchors:
	// 								[
	// 									[ "Perimeter", { shape: $(shapes[i]).attr("data-shape")  }],
	// 									[ "Perimeter", { shape: $(shapes[j]).attr( "data-shape") }]
	// 								]
	// 							});				
	// 						}	
	// 					}
					}
				});
			}
			
			window.jsPlumbCode = {
						
				init : function() {
					setInterval(pollServer, 15 * 1000);
					pollServer();
				}
			};
				
		
		})();
		
		jsPlumb.bind("ready", function() {
		
			// chrome fix.
			document.onselectstart = function () { return false; };
			
			jsPlumb.setRenderMode(jsPlumb.SVG);
			jsPlumbCode.init();
			
		});
		
		
		
		/**
		sprintf() for JavaScript 0.7-beta1
		http://www.diveintojavascript.com/projects/javascript-sprintf
		
		Copyright (c) Alexandru Marasteanu <alexaholic [at) gmail (dot] com>
		All rights reserved.
		
		Redistribution and use in source and binary forms, with or without
		modification, are permitted provided that the following conditions are met:
		* Redistributions of source code must retain the above copyright
		notice, this list of conditions and the following disclaimer.
		* Redistributions in binary form must reproduce the above copyright
		notice, this list of conditions and the following disclaimer in the
		documentation and/or other materials provided with the distribution.
		* Neither the name of sprintf() for JavaScript nor the
		names of its contributors may be used to endorse or promote products
		derived from this software without specific prior written permission.
		
		THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
		ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
		WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
		DISCLAIMED. IN NO EVENT SHALL Alexandru Marasteanu BE LIABLE FOR ANY
		DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
		(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
		LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
		ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
		(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
		SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
		
		
		Changelog:
		2010.09.06 - 0.7-beta1
		- features: vsprintf, support for named placeholders
		- enhancements: format cache, reduced global namespace pollution
		
		2010.05.22 - 0.6:
		- reverted to 0.4 and fixed the bug regarding the sign of the number 0
		Note:
		Thanks to Raphael Pigulla <raph (at] n3rd [dot) org> (http://www.n3rd.org/)
		who warned me about a bug in 0.5, I discovered that the last update was
		a regress. I appologize for that.
		
		2010.05.09 - 0.5:
		- bug fix: 0 is now preceeded with a + sign
		- bug fix: the sign was not at the right position on padded results (Kamal Abdali)
		- switched from GPL to BSD license
		
		2007.10.21 - 0.4:
		- unit test and patch (David Baird)
		
		2007.09.17 - 0.3:
		- bug fix: no longer throws exception on empty paramenters (Hans Pufal)
		
		2007.09.11 - 0.2:
		- feature: added argument swapping
		
		2007.04.03 - 0.1:
		- initial release
		**/
		
		window.sprintf = (function() {
			function get_type(variable) {
				return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();
			}
			function str_repeat(input, multiplier) {
				for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */}
				return output.join('');
			}
		
			var str_format = function() {
				if (!str_format.cache.hasOwnProperty(arguments[0])) {
					str_format.cache[arguments[0]] = str_format.parse(arguments[0]);
				}
				return str_format.format.call(null, str_format.cache[arguments[0]], arguments);
			};
		
			str_format.format = function(parse_tree, argv) {
				var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length;
				for (i = 0; i < tree_length; i++) {
					node_type = get_type(parse_tree[i]);
					if (node_type === 'string') {
						output.push(parse_tree[i]);
					}
					else if (node_type === 'array') {
						match = parse_tree[i]; // convenience purposes only
						if (match[2]) { // keyword argument
							arg = argv[cursor];
							for (k = 0; k < match[2].length; k++) {
								if (!arg.hasOwnProperty(match[2][k])) {
									throw(sprintf('[sprintf] property "%s" does not exist', match[2][k]));
								}
								arg = arg[match[2][k]];
							}
						}
						else if (match[1]) { // positional argument (explicit)
							arg = argv[match[1]];
						}
						else { // positional argument (implicit)
							arg = argv[cursor++];
						}
		
						if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) {
							throw(sprintf('[sprintf] expecting number but found %s', get_type(arg)));
						}
						switch (match[8]) {
							case 'b': arg = arg.toString(2); break;
							case 'c': arg = String.fromCharCode(arg); break;
							case 'd': arg = parseInt(arg, 10); break;
							case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break;
							case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break;
							case 'o': arg = arg.toString(8); break;
							case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break;
							case 'u': arg = Math.abs(arg); break;
							case 'x': arg = arg.toString(16); break;
							case 'X': arg = arg.toString(16).toUpperCase(); break;
						}
						arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg);
						pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' ';
						pad_length = match[6] - String(arg).length;
						pad = match[6] ? str_repeat(pad_character, pad_length) : '';
						output.push(match[5] ? arg + pad : pad + arg);
					}
				}
				return output.join('');
			};
		
			str_format.cache = {};
		
			str_format.parse = function(fmt) {
				var _fmt = fmt, match = [], parse_tree = [], arg_names = 0;
				while (_fmt) {
					if ((match = /^[^\x25]+/.exec(_fmt)) !== null) {
						parse_tree.push(match[0]);
					}
					else if ((match = /^\x25{2}/.exec(_fmt)) !== null) {
						parse_tree.push('%');
					}
					else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) {
						if (match[2]) {
							arg_names |= 1;
							var field_list = [], replacement_field = match[2], field_match = [];
							if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
								field_list.push(field_match[1]);
								while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
									if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
										field_list.push(field_match[1]);
									}
									else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) {
										field_list.push(field_match[1]);
									}
									else {
										throw('[sprintf] huh?');
									}
								}
							}
							else {
								throw('[sprintf] huh?');
							}
							match[2] = field_list;
						}
						else {
							arg_names |= 2;
						}
						if (arg_names === 3) {
							throw('[sprintf] mixing positional and named placeholders is not (yet) supported');
						}
						parse_tree.push(match);
					}
					else {
						throw('[sprintf] huh?');
					}
					_fmt = _fmt.substring(match[0].length);
				}
				return parse_tree;
			};
		
			return str_format;
		})();
		
		window.vsprintf = function(fmt, argv) {
			argv.unshift(fmt);
			return sprintf.apply(null, argv);
		};
		


		</script>
		<img src='/images/circle.png' style='display:none'>
		<script>setTimeout(function(){window.location.reload()}, 15 * 60 * 1000)</script>
	</body> 
</html>
